// Effect Classes.
// ===============

// The Particle Emitter Interface.
interface pp_particleEmitter {
  void init();
  void update();
  void setCurrentAmt(int amt);
  boolean getDestroyed();
  int getCurrentAmt();
}

// The localEffect Controller.
// localEffects are localized particle generators fixed to a portion of the level.
class pp_localEffect implements pp_particleEmitter {
  int x, y, spread, amt, rate, depth, currentAmt, currentRate;
  String xFormula, yFormula, sprite;
  boolean destroyed;
  
  int getCurrentAmt() {return currentAmt;}
  boolean getDestroyed() {return destroyed;}
  void setCurrentAmt(int amt) {currentAmt = amt;}
  
  pp_localEffect (int PosX, int PosY, int Spread, int Amt, int Rate, int Depth, String XFormula, String YFormula, String Sprite) {
    x = PosX;
    y = PosY;
    spread = Spread;
    depth = Depth;
    amt = Amt;
    rate = Rate;
    xFormula = XFormula;
    yFormula = YFormula;
    sprite = Sprite;
    
    // Initialize.
    init();
  }
  
  void init() {currentRate = rate;}
  
  void update() {
    
    // Check rate.
    if (currentRate < rate) {
      currentRate++;
    } else {
      currentRate = 0;
      
      // Check if we have under required amount of particles.
      if (currentAmt < amt) {
        int spawnX = int(x + random(0, spread) - spread/2);
        int spawnY = int(y + random(0, spread) - spread/2);
        
        // Create effect.
        pp_effects.add(
          new pp_effectBuilder()
            .x(spawnX)
            .y(spawnY)
            .depth(depth)
            .xFormula(xFormula)
            .yFormula(yFormula)
            .sprite(sprite)
            .parent(this)
            .isLooping(false)
            .build()
        );
        
        // Increase count by one.
        currentAmt++;
      }
    }
  }   
}

// The levelEffects Controller.
// levelEffects are level-wide particle effects like rain, snow, etc.
class pp_levelEffects implements pp_particleEmitter {
  int spawnEdge, amt, rate, depth, currentAmt, currentRate;
  float scroll;
  String xFormula, yFormula, sprite;
  boolean destroyed;
  
  int getCurrentAmt() {return currentAmt;}
  boolean getDestroyed() {return destroyed;}
  void setCurrentAmt(int amt) {currentAmt = amt;}
  
  pp_levelEffects (int SpawnEdge, int Amt, int Rate, int Depth, float Scroll, String XFormula, String YFormula, String Sprite) {
    spawnEdge = SpawnEdge;
    depth = Depth;
    amt = Amt;
    rate = Rate;
    scroll = Scroll;
    xFormula = XFormula;
    yFormula = YFormula;
    sprite = Sprite;
    
    // Initialize.
    init();
  }
  
  void init() {
    // Spawn initial load of particles.
    for (int i = 0; i < amt; i++) {
      pp_effects.add(
        new pp_effectBuilder()
          .x(int(random(0, pp_roomWidth)))
          .y(int(random(0, pp_roomHeight)))
          .depth(depth)
          .origin(5)
          .scroll(scroll)
          .xFormula(xFormula)
          .yFormula(yFormula)
          .sprite(sprite)
          .parent(this)
          .isLooping(true)
          .build()
      );
    }
    
    // Set as full.
    currentAmt = amt;
  }
  
  void update() {
    
    // Check rate.
    if (currentRate < rate) {
      currentRate++;
    } else {
      currentRate = 0;
      
      // Check if we have under required amount of particles.
      if (currentAmt < amt) {
        int spawnX = 0;
        int spawnY = 0;
        
        // Spawn on desired edge.
        switch (spawnEdge) {
          case 1:
            spawnX = int(random(0, pp_roomWidth));
            spawnY = 0;
            break;
            
          case 2:
            spawnX = pp_roomWidth;
            spawnY = int(random(0, pp_roomHeight));
            break;
            
          case 3:
            spawnX = int(random(0, pp_roomWidth));
            spawnY = pp_roomHeight;
            break;
            
          case 4:
            spawnX = 0;
            spawnY = int(random(0, pp_roomHeight));
            break;
        }
        
        // Create effect.
        pp_effects.add(
          new pp_effectBuilder()
            .x(spawnX)
            .y(spawnY)
            .depth(depth)
            .scroll(scroll)
            .xFormula(xFormula)
            .yFormula(yFormula)
            .sprite(sprite)
            .parent(this)
            .isLooping(true)
            .build()
        );
        
        // Increase count by one.
        currentAmt++;
      }
    }
  }   
}

// The Effect Builder class.
class pp_effectBuilder {
  int x, y, depth, origin;
  boolean isLooping, isMoving;
  float scroll;
  String xFormula, yFormula, sprite;
  pp_particleEmitter parent;
  
  pp_effectBuilder () {
    xFormula = "";
    yFormula = "";
  }
  pp_effectBuilder x(int PosX) {x = PosX; return this;}
  pp_effectBuilder y(int PosY) {y = PosY; return this;}
  pp_effectBuilder isLooping(boolean IsLooping) {isLooping = IsLooping; return this;}
  pp_effectBuilder depth(int renderDepth) {depth = renderDepth; return this;}
  pp_effectBuilder origin(int Origin) {origin = Origin; return this;}
  pp_effectBuilder scroll(float scrollSpeed) {scroll = scrollSpeed; origin = 5; return this;}
  pp_effectBuilder xFormula(String xFunction) {xFormula = xFunction; isMoving = true; origin = 5; return this;}
  pp_effectBuilder yFormula(String yFunction) {yFormula = yFunction; isMoving = true; origin = 5; return this;}
  pp_effectBuilder sprite(String spriteName) {sprite = spriteName; return this;}
  pp_effectBuilder parent(pp_particleEmitter Parent) {parent = Parent; return this;}
  
  pp_effect build() {
    return new pp_effect(this);
  }
}

// The Effect class
class pp_effect {
  int x, y, startX, startY, drawX, drawY;  // Positional variables.
  int objFrames, waitFrames, currentFrame, currentWait;  // Animation variables.
  int xOffset, yOffset, origin, depth, objWidth, objHeight, rand; 
  String sprite, yFormula, xFormula;
  boolean destroyed, isLooping, isMoving;
  float diffX, diffY, scroll;
  pp_particleEmitter parent;
  
  // Complete constructor.
  pp_effect (pp_effectBuilder builder) {
    
    // Set required variables.
    parent = builder.parent;
    x = 0;
    y = 0;
    startX = builder.x;
    startY = builder.y;
    rand = int(random(0, 100));
    depth = builder.depth;
    origin = builder.origin;
    isLooping = builder.isLooping;
    isMoving = builder.isMoving;
    
    // Process sprite.
    sprite = builder.sprite;
    pp_sprite pix = spr.get(sprite);
    objWidth = pix.pixWidth;
    objHeight = pix.sprite.height;
    objFrames = pix.pixFrames;
    waitFrames = pix.waitFrames;
    
    // Refresh offsets.
    xOffset = pp_getXOffset(origin, objWidth);
    yOffset = pp_getYOffset(origin, objHeight);
    drawX = startX - xOffset;
    drawY = startY - yOffset;
    
    // Process scroll factor.
    if (builder.scroll > 1) {
      scroll = builder.scroll;
      diffX = (scroll * pp_roomWidth) - pp_roomWidth;
      diffY = (scroll * pp_roomHeight) - pp_roomHeight;
    } else {
      scroll = 1;
    }
    
    // Process formulas.
    if (!builder.xFormula.equals("")) {
      xFormula = builder.xFormula;
    } else {
      xFormula = "x";
    }
    if (!builder.yFormula.equals("")) {
      yFormula = builder.yFormula;
    } else {
      yFormula = "y";
    }
  }
  
  // Update.
  void update() {
    if (isMoving) {
      x = pp_evaluate(xFormula, this);
      y = pp_evaluate(yFormula, this);
      
      drawX = pp_evaluate("startX + " + xFormula, this) - xOffset;
      drawY = pp_evaluate("startY + " + yFormula, this) - yOffset;
    }
  }
  
  void animate() {
    // Update frames according to FPS.
    if (currentWait < waitFrames) {
      currentWait++;
    } else {
      currentWait = 0;
      if (currentFrame < objFrames) {
        currentFrame++;
      } else {
        if (isLooping) {currentFrame = 0;}
        else {destroy();}
      }
    }
    
    // Destroy if is a particle and is out of screen.
    if (isLooping) {
      if (!pp_testOnLevel(startX + x, startY + y)) {
        destroy();
      }
    }
  }
  
  void draw() { 
    // Draw to screen.
    // If there's a scroll factor, draw particle at adjusted value.
    if (scroll > 1) {
      drawX = int((float(drawX) * scroll) - ((float(pp_displayX) / (pp_roomWidth - (width / pp_pixelSize))) * diffX));
      drawY = int((float(drawY) * scroll) - ((float(pp_displayY) / (pp_roomHeight - (height / pp_pixelSize))) * diffY));
      pp_drawSprite(drawX, drawY, depth, currentFrame, sprite);
      
    // Else, draw at original x and y value.
    } else {
      pp_drawSprite(drawX, drawY, depth, currentFrame, sprite);
    }
  }
  
  void destroy() {
    destroyed = true;
    if (parent != null) {
      parent.setCurrentAmt(parent.getCurrentAmt() - 1);
    }
  }
}    

// Effect Functions.
// =================

// Create Sprite Effect.
void pp_createEffect(int x, int y, int depth, int origin, String sprite) {
  pp_effects.add(
    new pp_effectBuilder()
      .x(x)
      .y(y)
      .depth(depth)
      .origin(origin)
      .sprite(sprite)
      .build()
  );
}

// Update all effects.
void pp_updateEffects() {
  for (int i = 0; i < pp_effects.size(); i++) { 
    
    // Get object.
    pp_effect obj = pp_effects.get(i);
    
    // If destroyed, remove from list.
    if (obj.destroyed){
      pp_effects.remove(i);
      i--;
      
    // Else, animate and render.
    } else {
      obj.update();
      obj.animate();
      obj.draw();
    }
  }
  
  // Update emitters.
  for (int i = 0; i < pp_emitters.size(); i++) {
    
    // Get emitter.
    pp_particleEmitter obj = pp_emitters.get(i);
    
    // If destroyed, remove from list.
    if (obj.getDestroyed()){
      pp_emitters.remove(i);
      i--;
    
    // Else, update. 
    } else {
      obj.update();
    }
  }    
}

int pp_evaluate(String formula, pp_effect obj) {
  try {
    Calculable calc =
      new ExpressionBuilder(formula)
        .withVariable("x", obj.x)
        .withVariable("y", obj.y)
        .withVariable("rand", obj.rand)
        .withVariable("startX", obj.startX)
        .withVariable("startY", obj.startY)
        .build();
    return (int)calc.calculate();
  } catch (Exception e) {
    return 0;
  }
}
